home *** CD-ROM | disk | FTP | other *** search
/ PC World Komputer 2010 April / PCWorld0410.iso / hity wydania / Ubuntu 9.10 PL / karmelkowy-koliberek-desktop-9.10-i386-PL.iso / casper / filesystem.squashfs / usr / share / pyshared / wadllib / application.py < prev    next >
Text File  |  2009-08-20  |  43KB  |  1,091 lines

  1. # Copyright 2008 Canonical Ltd.  All rights reserved.
  2.  
  3. # This file is part of wadllib.
  4. #
  5. # wadllib is free software: you can redistribute it and/or modify it under the
  6. # terms of the GNU Lesser General Public License as published by the Free
  7. # Software Foundation, version 3 of the License.
  8. #
  9. # wadllib is distributed in the hope that it will be useful, but WITHOUT ANY
  10. # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  11. # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
  12. # details.
  13. #
  14. # You should have received a copy of the GNU Lesser General Public License
  15. # along with wadllib. If not, see <http://www.gnu.org/licenses/>.
  16.  
  17. """Navigate the resources exposed by a web service.
  18.  
  19. The wadllib library helps a web client navigate the resources
  20. exposed by a web service. The service defines its resources in a
  21. single WADL file. wadllib parses this file and gives access to the
  22. resources defined inside. The client code can see the capabilities of
  23. a given resource and make the corresponding HTTP requests.
  24.  
  25. If a request returns a representation of the resource, the client can
  26. bind the string representation to the wadllib Resource object.
  27. """
  28.  
  29. __metaclass__ = type
  30.  
  31. __all__ = [
  32.     'Application',
  33.     'Link',
  34.     'Method',
  35.     'NoBoundRepresentationError',
  36.     'Parameter',
  37.     'RepresentationDefinition',
  38.     'ResponseDefinition',
  39.     'Resource',
  40.     'ResourceType',
  41.     'WADLError',
  42.     ]
  43.  
  44. from cStringIO import StringIO
  45. import datetime
  46. try:
  47.     from email.mime.multipart import MIMEMultipart
  48.     from email.mime.nonmultipart import MIMENonMultipart
  49. except ImportError:
  50.     # We must be on 2.4.
  51.     from email.MIMEMultipart import MIMEMultipart
  52.     from email.MIMENonMultipart import MIMENonMultipart
  53.  
  54. import time
  55. import urllib
  56. import simplejson
  57. try:
  58.     import xml.etree.cElementTree as ET
  59. except ImportError:
  60.     try:
  61.         import cElementTree as ET
  62.     except ImportError:
  63.         import elementtree.ElementTree as ET
  64. from lazr.uri import URI, merge
  65.  
  66. from iso_strptime import iso_strptime
  67.  
  68. NS_MAP = "xmlns:map"
  69. XML_SCHEMA_NS_URI = 'http://www.w3.org/2001/XMLSchema'
  70.  
  71. def wadl_tag(tag_name):
  72.     """Scope a tag name with the WADL namespace."""
  73.     return '{http://research.sun.com/wadl/2006/10}' + tag_name
  74.  
  75.  
  76. def wadl_xpath(tag_name):
  77.     """Turn a tag name into an XPath path."""
  78.     return './' + wadl_tag(tag_name)
  79.  
  80.  
  81. def _merge_dicts(*dicts):
  82.     """Merge any number of dictionaries, some of which may be None."""
  83.     final = {}
  84.     for dict in dicts:
  85.         if dict is not None:
  86.             final.update(dict)
  87.     return final
  88.  
  89.  
  90. class WADLError(Exception):
  91.     """An exception having to do with the state of the WADL application."""
  92.     pass
  93.  
  94.  
  95. class NoBoundRepresentationError(WADLError):
  96.     """An unbound resource was used where wadllib expected a bound resource.
  97.  
  98.     To obtain the value of a resource's parameter, you first must bind
  99.     the resource to a representation. Otherwise the resource has no
  100.     idea what the value is and doesn't even know if you've given it a
  101.     parameter name that makes sense.
  102.     """
  103.  
  104.  
  105. class UnsupportedMediaTypeError(WADLError):
  106.     """A media type was given that's not supported in this context.
  107.  
  108.     A resource can only be bound to media types it has representations
  109.     of.
  110.     """
  111.  
  112.  
  113. class WADLBase(object):
  114.     """A base class for objects that contain WADL-derived information."""
  115.  
  116.  
  117. class HasParametersMixin:
  118.     """A mixin class for objects that have associated Parameter objects."""
  119.  
  120.     def params(self, styles, resource=None):
  121.         """Find subsidiary parameters that have the given styles."""
  122.         if resource is None:
  123.             resource = self.resource
  124.         if resource is None:
  125.             raise ValueError("Could not find any particular resource")
  126.         if self.tag is None:
  127.             return []
  128.         param_tags = self.tag.findall(wadl_xpath('param'))
  129.         if param_tags is None:
  130.             return []
  131.         return [Parameter(resource, param_tag)
  132.                 for param_tag in param_tags
  133.                 if param_tag.attrib.get('style') in styles]
  134.  
  135.     def validate_param_values(self, params, param_values,
  136.                               enforce_completeness=True, **kw_param_values):
  137.         """Make sure the given valueset is valid.
  138.  
  139.         A valueset might be invalid because it contradicts a fixed
  140.         value or (if enforce_completeness is True) because it lacks a
  141.         required value.
  142.  
  143.         :param params: A list of Parameter objects.
  144.         :param param_values: A dictionary of parameter values. May include
  145.            paramters whose names are not valid Python identifiers.
  146.         :param enforce_completeness: If True, this method will raise
  147.            an exception when the given value set lacks a value for a
  148.            required parameter.
  149.         :param kw_param_values: A dictionary of parameter values.
  150.         :return: A dictionary of validated parameter values.
  151.         """
  152.         param_values = _merge_dicts(param_values, kw_param_values)
  153.         validated_values = {}
  154.         for param in params:
  155.             name = param.name
  156.             if param.fixed_value is not None:
  157.                 if (name in param_values
  158.                     and param_values[name] != param.fixed_value):
  159.                     raise ValueError(("Value '%s' for parameter '%s' "
  160.                                       "conflicts with fixed value '%s'")
  161.                                      % (param_values[name], name,
  162.                                         param.fixed_value))
  163.                 param_values[name] = param.fixed_value
  164.             options = [option.value for option in param.options]
  165.             if (len(options) > 0 and name in param_values
  166.                 and param_values[name] not in options):
  167.                 raise ValueError(("Invalid value '%s' for parameter '%s': "
  168.                                   'valid values are: "%s"') % (
  169.                         param_values[name], name, '", "'.join(options)))
  170.             if (enforce_completeness and param.is_required
  171.                 and not name in param_values):
  172.                 raise ValueError("No value for required parameter '%s'"
  173.                                  % name)
  174.             if name in param_values:
  175.                 validated_values[name] = param_values[name]
  176.                 del param_values[name]
  177.         if len(param_values) > 0:
  178.             raise ValueError("Unrecognized parameter(s): '%s'"
  179.                              % "', '".join(param_values.keys()))
  180.         return validated_values
  181.  
  182.  
  183. class WADLResolvableDefinition(WADLBase):
  184.     """A base class for objects whose definitions may be references."""
  185.  
  186.     def __init__(self, application):
  187.         """Initialize with a WADL application.
  188.  
  189.         :param application: A WADLDefinition. Relative links are
  190.             assumed to be relative to this object's URL.
  191.         """
  192.         self._definition = None
  193.         self.application = application
  194.  
  195.     def resolve_definition(self):
  196.         """Return the definition of this object, wherever it is.
  197.  
  198.         Resource is a good example. A WADL <resource> tag
  199.         may contain a large number of nested tags describing a
  200.         resource, or it may just contain a 'type' attribute that
  201.         references a <resource_type> which contains those same
  202.         tags. Resource.resolve_definition() will return the original
  203.         Resource object in the first case, and a
  204.         ResourceType object in the second case.
  205.         """
  206.         if self._definition is not None:
  207.             return self._definition
  208.         object_url = self._get_definition_url()
  209.         if object_url is None:
  210.             # The object contains its own definition.
  211.             # XXX leonardr 2008-05-28:
  212.             # This code path is not tested in Launchpad.
  213.             self._definition = self
  214.             return self
  215.         # The object makes reference to some other object. Resolve
  216.         # its URL and return it.
  217.         xml_id = self.application.lookup_xml_id(object_url)
  218.         definition = self._definition_factory(xml_id)
  219.         if definition is None:
  220.             # XXX leonardr 2008-06-
  221.             # This code path is not tested in Launchpad.
  222.             # It requires an invalid WADL file that makes
  223.             # a reference to a nonexistent tag within the
  224.             # same WADL file.
  225.             raise KeyError('No such XML ID: "%s"' % object_url)
  226.         self._definition = definition
  227.         return definition
  228.  
  229.     def _definition_factory(self, id):
  230.         """Transform an XML ID into a wadllib wrapper object.
  231.  
  232.         Which kind of object it is depends on the subclass.
  233.         """
  234.         raise NotImplementedError()
  235.  
  236.     def _get_definition_url(self):
  237.         """Find the URL that identifies an external reference.
  238.  
  239.         How to do this depends on the subclass.
  240.         """
  241.         raise NotImplementedError()
  242.  
  243.  
  244. class Resource(WADLResolvableDefinition):
  245.     """A resource, possibly bound to a representation."""
  246.  
  247.     def __init__(self, application, url, resource_type,
  248.                  representation=None, media_type=None,
  249.                  representation_needs_processing=True,
  250.                  representation_definition=None):
  251.         """
  252.         :param application: A WADLApplication.
  253.         :param url: The URL to this resource.
  254.         :param resource_type: An ElementTree <resource> or <resource_type> tag.
  255.         :param representation: A string representation.
  256.         :param media_type: The media type of the representation.
  257.         :param representation_needs_processing: Set to False if the
  258.             'representation' parameter should be used as
  259.             is. Otherwise, it will be transformed from a string into
  260.             an appropriate Python data structure, depending on its
  261.             media type.
  262.         :param representation_definition: A RepresentationDefinition
  263.             object describing the structure of this
  264.             representation. Used in cases when the representation
  265.             isn't the result of sending a standard GET to the
  266.             resource.
  267.         """
  268.         super(Resource, self).__init__(application)
  269.         self._url = url
  270.         if isinstance(resource_type, basestring):
  271.             # We were passed the URL to a resource type. Look up the
  272.             # type object itself
  273.             self.tag = self.application.get_resource_type(resource_type).tag
  274.         else:
  275.             # We were passed an XML tag that describes a resource or
  276.             # resource type.
  277.             self.tag = resource_type
  278.  
  279.         self.representation = None
  280.         if representation is not None:
  281.             if media_type == 'application/json':
  282.                 if representation_needs_processing:
  283.                     self.representation = simplejson.loads(unicode(representation))
  284.                 else:
  285.                     self.representation = representation
  286.             else:
  287.                 raise UnsupportedMediaTypeError(
  288.                     "This resource doesn't define a representation for "
  289.                     "media type %s" % media_type)
  290.         self.media_type = media_type
  291.         if representation is not None:
  292.             if representation_definition is not None:
  293.                 self.representation_definition = representation_definition
  294.             else:
  295.                 self.representation_definition = (
  296.                     self.get_representation_definition(self.media_type))
  297.  
  298.     @property
  299.     def url(self):
  300.         """Return the URL to this resource."""
  301.         return self._url
  302.  
  303.     @property
  304.     def type_url(self):
  305.         """Return the URL to the type definition for this resource, if any."""
  306.         if self.tag is None:
  307.             return None
  308.         url = self.tag.attrib.get('type')
  309.         if url is not None:
  310.             # This resource is defined in the WADL file.
  311.             return url
  312.         type_id = self.tag.attrib.get('id')
  313.         if type_id is not None:
  314.             # This resource was obtained by following a link.
  315.             base = URI(self.application.markup_url).ensureSlash()
  316.             return str(base) + '#' + type_id
  317.  
  318.         # This resource does not have any associated resource type.
  319.         return None
  320.  
  321.     @property
  322.     def id(self):
  323.         """Return the ID of this resource."""
  324.         return self.tag.attrib['id']
  325.  
  326.     def bind(self, representation, media_type='application/json',
  327.              representation_needs_processing=True,
  328.              representation_definition=None):
  329.         """Bind the resource to a representation of that resource.
  330.  
  331.         :param representation: A string representation
  332.         :param media_type: The media type of the representation.
  333.         :param representation_needs_processing: Set to False if the
  334.             'representation' parameter should be used as
  335.             is.
  336.         :param representation_definition: A RepresentationDefinition
  337.             object describing the structure of this
  338.             representation. Used in cases when the representation
  339.             isn't the result of sending a standard GET to the
  340.             resource.
  341.         :return: A Resource bound to a particular representation.
  342.         """
  343.         return Resource(self.application, self.url, self.tag,
  344.                         representation, media_type,
  345.                         representation_needs_processing,
  346.                         representation_definition)
  347.  
  348.     def get_representation_definition(self, media_type):
  349.         """Get a description of one of this resource's representations."""
  350.         default_get_response = self.get_method('GET').response
  351.         for representation in default_get_response:
  352.             representation_tag = representation.resolve_definition().tag
  353.             if representation_tag.attrib.get('mediaType') == media_type:
  354.                 return representation
  355.         raise UnsupportedMediaTypeError("No definition for representation "
  356.                                         "with media type %s." % media_type)
  357.  
  358.     def get_method(self, http_method=None, media_type=None, query_params=None,
  359.                    representation_params=None):
  360.         """Look up one of this resource's methods by HTTP method.
  361.  
  362.         :param http_method: The HTTP method used to invoke the desired
  363.                             method. Case-insensitive and optional.
  364.  
  365.         :param media_type: The media type of the representation
  366.                            accepted by the method. Optional.
  367.  
  368.         :param query_params: The names and values of any fixed query
  369.                              parameters used to distinguish between
  370.                              two methods that use the same HTTP
  371.                              method. Optional.
  372.  
  373.         :param representation_params: The names and values of any
  374.                              fixed representation parameters used to
  375.                              distinguish between two methods that use
  376.                              the same HTTP method and have the same
  377.                              media type. Optional.
  378.  
  379.         :return: A MethodDefinition, or None if there's no definition
  380.                   that fits the given constraints.
  381.         """
  382.         for method_tag in self._method_tag_iter():
  383.             name = method_tag.attrib.get('name', '').lower()
  384.             if http_method is None or name == http_method.lower():
  385.                 method = Method(self, method_tag)
  386.                 if method.is_described_by(media_type, query_params,
  387.                                           representation_params):
  388.                     return method
  389.         return None
  390.  
  391.     def parameter_names(self, media_type=None):
  392.         """A list naming this resource's parameters.
  393.  
  394.         :param media_type: Media type of the representation definition
  395.             whose parameters are being named. Must be present unless
  396.             this resource is bound to a representation.
  397.  
  398.         :raise NoBoundRepresentationError: If this resource is not
  399.             bound to a representation and media_type was not provided.
  400.         """
  401.         return self._find_representation_definition(
  402.             media_type).parameter_names(self)
  403.  
  404.     @property
  405.     def method_iter(self):
  406.         """An iterator over the methods defined on this resource."""
  407.         for method_tag in self._method_tag_iter():
  408.             yield Method(self, method_tag)
  409.  
  410.     def get_parameter(self, param_name, media_type=None):
  411.         """Find a parameter within a representation definition.
  412.  
  413.         :param param_name: Name of the parameter to find.
  414.  
  415.         :param media_type: Media type of the representation definition
  416.             whose parameters are being named. Must be present unless
  417.             this resource is bound to a representation.
  418.  
  419.         :raise NoBoundRepresentationError: If this resource is not
  420.             bound to a representation and media_type was not provided.
  421.         """
  422.         definition = self._find_representation_definition(media_type)
  423.         representation_tag = definition.tag
  424.         for param_tag in representation_tag.findall(wadl_xpath('param')):
  425.             if param_tag.attrib.get('name') == param_name:
  426.                 return Parameter(self, param_tag)
  427.         return None
  428.  
  429.     def get_parameter_value(self, parameter):
  430.         """Find the value of a parameter, given the Parameter object.
  431.  
  432.         :raise ValueError: If the parameter value can't be converted into
  433.         its defined type.
  434.         """
  435.  
  436.         if self.representation is None:
  437.             raise NoBoundRepresentationError(
  438.                 "Resource is not bound to any representation.")
  439.         if self.media_type == 'application/json':
  440.             # XXX leonardr 2008-05-28 A real JSONPath implementation
  441.             # should go here. It should execute tag.attrib['path']
  442.             # against the JSON representation.
  443.             #
  444.             # Right now the implementation assumes the JSON
  445.             # representation is a hash and treats tag.attrib['name'] as a
  446.             # key into the hash.
  447.             if parameter.style != 'plain':
  448.                 raise NotImplementedError(
  449.                     "Don't know how to find value for a parameter of "
  450.                     "type %s." % parameter.style)
  451.             value = self.representation[parameter.name]
  452.             if value is not None:
  453.                 namespace_url, data_type = self._dereference_namespace(
  454.                     parameter.tag, parameter.type)
  455.                 if (namespace_url == XML_SCHEMA_NS_URI
  456.                     and data_type in ['dateTime', 'date']):
  457.                     try:
  458.                         # Parse it as an ISO 8601 date and time.
  459.                         value = iso_strptime(value)
  460.                     except ValueError:
  461.                         # Parse it as an ISO 8601 date.
  462.                         try:
  463.                             value = datetime.datetime(
  464.                                 *(time.strptime(value, "%Y-%m-%d")[0:6]))
  465.                         except ValueError:
  466.                             # Raise an unadorned ValueError so the client
  467.                             # can treat the value as a string if they
  468.                             # want.
  469.                             raise ValueError(value)
  470.             return value
  471.  
  472.         raise NotImplementedError("Path traversal not implemented for "
  473.                                   "a representation of media type %s."
  474.                                   % self.media_type)
  475.  
  476.  
  477.     def _dereference_namespace(self, tag, value):
  478.         """Splits a value into namespace URI and value.
  479.  
  480.         :param tag: A tag to use as context when mapping namespace
  481.         names to URIs.
  482.         """
  483.         if value is not None and ':' in value:
  484.             namespace, value = value.split(':', 1)
  485.         else:
  486.             namespace = ''
  487.         ns_map = tag.get(NS_MAP)
  488.         namespace_url = ns_map.get(namespace, None)
  489.         return namespace_url, value
  490.  
  491.     def _definition_factory(self, id):
  492.         """Given an ID, find a ResourceType for that ID."""
  493.         return self.application.resource_types.get(id)
  494.  
  495.     def _get_definition_url(self):
  496.         """Return the URL that shows where a resource is 'really' defined.
  497.  
  498.         If a resource's capabilities are defined by reference, the
  499.         <resource> tag's 'type' attribute will contain the URL to the
  500.         <resource_type> that defines them.
  501.         """
  502.         return self.tag.attrib.get('type')
  503.  
  504.     def _find_representation_definition(self, media_type=None):
  505.         """Get the most appropriate representation definition.
  506.  
  507.         If media_type is provided, the most appropriate definition is
  508.         the definition of the representation of that media type.
  509.  
  510.         If this resource is bound to a representation, the most
  511.         appropriate definition is the definition of that
  512.         representation. Otherwise, the most appropriate definition is
  513.         the definition of the representation served in response to a
  514.         standard GET.
  515.  
  516.         :param media_type: Media type of the definition to find. Must
  517.             be present unless the resource is bound to a
  518.             representation.
  519.  
  520.         :raise NoBoundRepresentationError: If this resource is not
  521.             bound to a representation and media_type was not provided.
  522.  
  523.         :return: A RepresentationDefinition
  524.         """
  525.         if media_type is not None:
  526.             definition = self.get_representation_definition(media_type)
  527.         elif self.representation is not None:
  528.             definition = self.representation_definition.resolve_definition()
  529.         else:
  530.             raise NoBoundRepresentationError(
  531.                 "Resource is not bound to any representation, and no media "
  532.                 "media type was specified.")
  533.         return definition.resolve_definition()
  534.  
  535.  
  536.     def _method_tag_iter(self):
  537.         """Iterate over this resource's <method> tags."""
  538.         definition = self.resolve_definition().tag
  539.         for method_tag in definition.findall(wadl_xpath('method')):
  540.             yield method_tag
  541.  
  542.  
  543. class Method(WADLBase):
  544.     """A wrapper around an XML <method> tag.
  545.     """
  546.     def __init__(self, resource, method_tag):
  547.         """Initialize with a <method> tag.
  548.  
  549.         :param method_tag: An ElementTree <method> tag.
  550.         """
  551.         self.resource = resource
  552.         self.application = self.resource.application
  553.         self.tag = method_tag
  554.  
  555.     @property
  556.     def request(self):
  557.         """Return the definition of a request that invokes the WADL method."""
  558.         return RequestDefinition(self, self.tag.find(wadl_xpath('request')))
  559.  
  560.     @property
  561.     def response(self):
  562.         """Return the definition of the response to the WADL method."""
  563.         return ResponseDefinition(self.resource,
  564.                                   self.tag.find(wadl_xpath('response')))
  565.  
  566.     @property
  567.     def id(self):
  568.         """The XML ID of the WADL method definition."""
  569.         return self.tag.attrib.get('id')
  570.  
  571.     @property
  572.     def name(self):
  573.         """The name of the WADL method definition.
  574.  
  575.         This is also the name of the HTTP method (GET, POST, etc.)
  576.         that should be used to invoke the WADL method.
  577.         """
  578.         return self.tag.attrib.get('name').lower()
  579.  
  580.     def build_request_url(self, param_values=None, **kw_param_values):
  581.         """Return the request URL to use to invoke this method."""
  582.         return self.request.build_url(param_values, **kw_param_values)
  583.  
  584.     def build_representation(self, media_type=None,
  585.                              param_values=None, **kw_param_values):
  586.         """Build a representation to be sent when invoking this method.
  587.  
  588.         :return: A 2-tuple of (media_type, representation).
  589.         """
  590.         return self.request.representation(
  591.             media_type, param_values, **kw_param_values)
  592.  
  593.     def is_described_by(self, media_type=None, query_values=None,
  594.                         representation_values=None):
  595.         """Returns true if this method fits the given constraints.
  596.  
  597.         :param media_type: The method must accept this media type as a
  598.                            representation.
  599.  
  600.         :param query_values: These key-value pairs must be acceptable
  601.                            as values for this method's query
  602.                            parameters. This need not be a complete set
  603.                            of parameters acceptable to the method.
  604.  
  605.         :param representation_values: These key-value pairs must be
  606.                            acceptable as values for this method's
  607.                            representation parameters. Again, this need
  608.                            not be a complete set of parameters
  609.                            acceptable to the method.
  610.         """
  611.         representation = None
  612.         if media_type is not None:
  613.             representation = self.request.get_representation_definition(
  614.                 media_type)
  615.             if representation is None:
  616.                 return False
  617.  
  618.         if query_values is not None and len(query_values) > 0:
  619.             request = self.request
  620.             if request is None:
  621.                 # This method takes no special request
  622.                 # parameters, so it can't match.
  623.                 return False
  624.             try:
  625.                 request.validate_param_values(
  626.                     request.query_params, query_values, False)
  627.             except ValueError:
  628.                 return False
  629.  
  630.         # At this point we know the media type and query values match.
  631.         if (representation_values is None
  632.             or len(representation_values) == 0):
  633.             return True
  634.  
  635.         if representation is not None:
  636.             return representation.is_described_by(
  637.                 representation_values)
  638.         for representation in self.request.representations:
  639.             try:
  640.                 representation.validate_param_values(
  641.                     representation.params(self.resource),
  642.                     representation_values, False)
  643.                 return True
  644.             except ValueError:
  645.                 pass
  646.         return False
  647.  
  648.  
  649. class RequestDefinition(WADLBase, HasParametersMixin):
  650.     """A wrapper around the description of the request invoking a method."""
  651.     def __init__(self, method, request_tag):
  652.         """Initialize with a <request> tag.
  653.  
  654.         :param resource: The resource to which this request can be sent.
  655.         :param request_tag: An ElementTree <request> tag.
  656.         """
  657.         self.method = method
  658.         self.resource = self.method.resource
  659.         self.application = self.resource.application
  660.         self.tag = request_tag
  661.  
  662.     @property
  663.     def query_params(self):
  664.         """Return the query parameters for this method."""
  665.         return self.params(['query'])
  666.  
  667.     @property
  668.     def representations(self):
  669.         for definition in self.tag.findall(wadl_xpath('representation')):
  670.             yield RepresentationDefinition(
  671.                 self.application, self.resource, definition)
  672.  
  673.     def get_representation_definition(self, media_type=None):
  674.         """Return the appropriate representation definition."""
  675.         for representation in self.representations:
  676.             if media_type is None or representation.media_type == media_type:
  677.                 return representation
  678.         return None
  679.  
  680.     def representation(self, media_type=None, param_values=None,
  681.                        **kw_param_values):
  682.         """Build a representation to be sent along with this request.
  683.  
  684.         :return: A 2-tuple of (media_type, representation).
  685.         """
  686.         definition = self.get_representation_definition(media_type)
  687.         if definition is None:
  688.             raise TypeError("Cannot build representation of media type %s"
  689.                             % media_type)
  690.         return definition.bind(param_values, **kw_param_values)
  691.  
  692.     def build_url(self, param_values=None, **kw_param_values):
  693.         """Return the request URL to use to invoke this method."""
  694.         validated_values = self.validate_param_values(
  695.             self.query_params, param_values, **kw_param_values)
  696.         url = self.resource.url
  697.         if len(validated_values) > 0:
  698.             if '?' in url:
  699.                 append = '&'
  700.             else:
  701.                 append = '?'
  702.             url += append + urllib.urlencode(validated_values)
  703.         return url
  704.  
  705.  
  706. class ResponseDefinition(HasParametersMixin):
  707.     """A wrapper around the description of a response to a method."""
  708.  
  709.     # XXX leonardr 2008-05-29 it would be nice to have
  710.     # ResponseDefinitions for POST operations and nonstandard GET
  711.     # operations say what representations and/or status codes you get
  712.     # back. Getting this to work with Launchpad requires work on the
  713.     # Launchpad side.
  714.     def __init__(self, resource, response_tag, headers=None):
  715.         """Initialize with a <response> tag.
  716.  
  717.         :param response_tag: An ElementTree <response> tag.
  718.         """
  719.         self.application = resource.application
  720.         self.resource = resource
  721.         self.tag = response_tag
  722.         self.headers = headers
  723.  
  724.     def __iter__(self):
  725.         """Get an iterator over the representation definitions.
  726.  
  727.         These are the representations returned in response to an
  728.         invocation of this method.
  729.         """
  730.         path = wadl_xpath('representation')
  731.         for representation_tag in self.tag.findall(path):
  732.             yield RepresentationDefinition(
  733.                 self.resource.application, self.resource, representation_tag)
  734.  
  735.     def bind(self, headers):
  736.         """Bind the response to a set of HTTP headers.
  737.  
  738.         A WADL response can have associated header parameters, but no
  739.         other kind.
  740.         """
  741.         return ResponseDefinition(self.resource, self.tag, headers)
  742.  
  743.     def get_parameter(self, param_name):
  744.         """Find a header parameter within the response."""
  745.         for param_tag in self.tag.findall(wadl_xpath('param')):
  746.             if (param_tag.attrib.get('name') == param_name
  747.                 and param_tag.attrib.get('style') == 'header'):
  748.                 return Parameter(self, param_tag)
  749.         return None
  750.  
  751.     def get_parameter_value(self, parameter):
  752.         """Find the value of a parameter, given the Parameter object."""
  753.         if self.headers is None:
  754.             raise NoBoundRepresentationError(
  755.                 "Response object is not bound to any headers.")
  756.         if parameter.style != 'header':
  757.             raise NotImplementedError(
  758.                 "Don't know how to find value for a parameter of "
  759.                 "type %s." % parameter.style)
  760.         return self.headers.get(parameter.name)
  761.  
  762.     def get_representation_definition(self, media_type):
  763.         """Get one of the possible representations of the response."""
  764.         if self.tag is None:
  765.             return None
  766.         for representation in self:
  767.             if representation.media_type == media_type:
  768.                 return representation
  769.         return None
  770.  
  771.  
  772. class RepresentationDefinition(WADLResolvableDefinition, HasParametersMixin):
  773.     """A definition of the structure of a representation."""
  774.  
  775.     def __init__(self, application, resource, representation_tag):
  776.         super(RepresentationDefinition, self).__init__(application)
  777.         self.resource = resource
  778.         self.tag = representation_tag
  779.  
  780.     def params(self, resource):
  781.         return super(RepresentationDefinition, self).params(
  782.             ['query', 'plain'], resource)
  783.  
  784.     def parameter_names(self, resource):
  785.         """Return the names of all parameters."""
  786.         return [param.name for param in self.params(resource)]
  787.  
  788.     @property
  789.     def media_type(self):
  790.         """The media type of the representation described here."""
  791.         return self.resolve_definition().tag.attrib['mediaType']
  792.  
  793.     def bind(self, param_values, **kw_param_values):
  794.         """Bind the definition to parameter values, creating a document.
  795.  
  796.         :return: A 2-tuple (media_type, document).
  797.         """
  798.         definition = self.resolve_definition()
  799.         params = definition.params(self.resource)
  800.         validated_values = self.validate_param_values(
  801.             params, param_values, **kw_param_values)
  802.         media_type = self.media_type
  803.         if media_type == 'application/x-www-form-urlencoded':
  804.             doc = urllib.urlencode(validated_values)
  805.         elif media_type == 'multipart/form-data':
  806.             outer = MIMEMultipart()
  807.             outer.set_type('multipart/form-data')
  808.             for param in params:
  809.                 value = validated_values[param.name]
  810.                 if param.type == 'binary':
  811.                     maintype, subtype = 'application', 'octet-stream'
  812.                     params = {}
  813.                 else:
  814.                     maintype, subtype = 'text', 'plain'
  815.                     params = {'charset' : 'utf-8'}
  816.                 inner = MIMENonMultipart(maintype, subtype, **params)
  817.                 inner.set_payload(value)
  818.                 inner['Content-Disposition'] = (
  819.                     'form-data; name="%s"' % param.name)
  820.                 outer.attach(inner)
  821.             doc = str(outer)
  822.             # Chop off the 'From' line, which only makes sense in an
  823.             # email.
  824.             doc = doc[doc.find('\n')+1:]
  825.             media_type = (outer.get_content_type() +
  826.                           '; boundary="%s"' % outer.get_boundary())
  827.         elif media_type == 'application/json':
  828.             doc = simplejson.dumps(validated_values)
  829.         else:
  830.             raise ValueError("Unsupported media type: '%s'" % media_type)
  831.         return media_type, doc
  832.  
  833.     def _definition_factory(self, id):
  834.         """Turn a representation ID into a RepresentationDefinition."""
  835.         return self.application.representation_definitions.get(id)
  836.  
  837.     def _get_definition_url(self):
  838.         """Find the URL containing the representation's 'real' definition.
  839.  
  840.         If a representation's structure is defined by reference, the
  841.         <representation> tag's 'href' attribute will contain the URL
  842.         to the <representation> that defines the structure.
  843.         """
  844.         return self.tag.attrib.get('href')
  845.  
  846.  
  847. class Parameter(WADLBase):
  848.     """One of the parameters of a representation definition."""
  849.  
  850.     def __init__(self, value_container, tag):
  851.         """Initialize with respect to a value container.
  852.  
  853.         :param value_container: Usually the resource whose representation
  854.             has this parameter. If the resource is bound to a representation,
  855.             you'll be able to find the value of this parameter in the
  856.             representation. This may also be a server response whose headers
  857.             define a value for this parameter.
  858.         :tag: The ElementTree <param> tag for this parameter.
  859.         """
  860.         self.application = value_container.application
  861.         self.value_container = value_container
  862.         self.tag = tag
  863.  
  864.     @property
  865.     def name(self):
  866.         """The name of this parameter."""
  867.         return self.tag.attrib.get('name')
  868.  
  869.     @property
  870.     def style(self):
  871.         """The style of this parameter."""
  872.         return self.tag.attrib.get('style')
  873.  
  874.     @property
  875.     def type(self):
  876.         """The XSD type of this parameter."""
  877.         return self.tag.attrib.get('type')
  878.  
  879.     @property
  880.     def fixed_value(self):
  881.         """The value to which this parameter is fixed, if any.
  882.  
  883.         A fixed parameter must be present in invocations of a WADL
  884.         method, and it must have a particular value. This is commonly
  885.         used to designate one parameter as containing the name of the
  886.         server-side operation to be invoked.
  887.         """
  888.         return self.tag.attrib.get('fixed')
  889.  
  890.     @property
  891.     def is_required(self):
  892.         """Whether or not a value for this parameter is required."""
  893.         return (self.tag.attrib.get('required', 'false').lower()
  894.                 in ['1', 'true'])
  895.  
  896.     def get_value(self):
  897.         """The value of this parameter in the bound representation/headers.
  898.  
  899.         :raise NoBoundRepresentationError: If this parameter's value
  900.                container is not bound to a representation or a set of
  901.                headers.
  902.         """
  903.         return self.value_container.get_parameter_value(self)
  904.  
  905.     @property
  906.     def options(self):
  907.         """Return the set of acceptable values for this parameter."""
  908.         return [Option(self, option_tag)
  909.                 for option_tag  in self.tag.findall(wadl_xpath('option'))]
  910.  
  911.     @property
  912.     def linked_resource(self):
  913.         """Find the type of resource linked to by this parameter.
  914.  
  915.         This only works for parameters whose WADL definition includes a
  916.         <link> tag that points to a known WADL description.
  917.  
  918.         :return: A Resource object for the resource at the other end
  919.         of the link.
  920.         """
  921.         link_tag = self.tag.find(wadl_xpath('link'))
  922.         if link_tag is None:
  923.             raise ValueError("This parameter isn't a link to anything.")
  924.         return Link(self, link_tag).resolve_definition()
  925.  
  926. class Option(WADLBase):
  927.     """One of a set of possible values for a parameter."""
  928.  
  929.     def __init__(self, parameter, option_tag):
  930.         """Initialize the option.
  931.  
  932.         :param parameter: A Parameter.
  933.         :param link_tag: An ElementTree <option> tag.
  934.         """
  935.         self.parameter = parameter
  936.         self.tag = option_tag
  937.  
  938.     @property
  939.     def value(self):
  940.         return self.tag.attrib.get('value')
  941.  
  942.  
  943. class Link(WADLResolvableDefinition):
  944.     """A link from one resource to another.
  945.  
  946.     Calling resolve_definition() on a Link will give you a Resource for the
  947.     type of resource linked to.
  948.     """
  949.  
  950.     def __init__(self, parameter, link_tag):
  951.         """Initialize the link.
  952.  
  953.         :param parameter: A Parameter.
  954.         :param link_tag: An ElementTree <link> tag.
  955.         """
  956.         super(Link, self).__init__(parameter.application)
  957.         self.parameter = parameter
  958.         self.tag = link_tag
  959.  
  960.     def _definition_factory(self, id):
  961.         """Turn a resource type ID into a ResourceType."""
  962.         return Resource(
  963.             self.application, self.parameter.get_value(),
  964.             self.application.resource_types.get(id).tag)
  965.  
  966.     def _get_definition_url(self):
  967.         """Find the URL containing the definition ."""
  968.         type = self.tag.attrib.get('resource_type')
  969.         if type is None:
  970.             raise WADLError("Parameter is a link, but not to a resource "
  971.                             "with a known WADL description.")
  972.         return type
  973.  
  974.  
  975. class ResourceType(WADLBase):
  976.     """A wrapper around an XML <resource_type> tag."""
  977.  
  978.     def __init__(self, resource_type_tag):
  979.         """Initialize with a <resource_type> tag.
  980.  
  981.         :param resource_type_tag: An ElementTree <resource_type> tag.
  982.         """
  983.         self.tag = resource_type_tag
  984.  
  985.  
  986. class Application(WADLBase):
  987.     """A WADL document made programmatically accessible."""
  988.  
  989.     def __init__(self, markup_url, markup):
  990.         """Parse WADL and find the most important parts of the document.
  991.  
  992.         :param markup_url: The URL from which this document was obtained.
  993.         :param markup: The WADL markup itself, or an open filehandle to it.
  994.         """
  995.         self.markup_url = markup_url
  996.         if hasattr(markup, 'read'):
  997.             markup = markup.read()
  998.         self.doc = self._from_string(markup)
  999.         self.resources = self.doc.find(wadl_xpath('resources'))
  1000.         self.resource_base = self.resources.attrib.get('base')
  1001.         self.representation_definitions = {}
  1002.         self.resource_types = {}
  1003.         for representation in self.doc.findall(wadl_xpath('representation')):
  1004.             id = representation.attrib.get('id')
  1005.             if id is not None:
  1006.                 definition = RepresentationDefinition(
  1007.                     self, None, representation)
  1008.                 self.representation_definitions[id] = definition
  1009.         for resource_type in self.doc.findall(wadl_xpath('resource_type')):
  1010.             id = resource_type.attrib['id']
  1011.             self.resource_types[id] = ResourceType(resource_type)
  1012.  
  1013.     def _from_string(self, markup):
  1014.         """Turns markup into a document.
  1015.  
  1016.         Just a wrapper around ElementTree which keeps track of namespaces.
  1017.         """
  1018.         events = "start", "start-ns", "end-ns"
  1019.         root = None
  1020.         ns_map = []
  1021.  
  1022.         for event, elem in ET.iterparse(StringIO(markup), events):
  1023.             if event == "start-ns":
  1024.                 ns_map.append(elem)
  1025.             elif event == "end-ns":
  1026.                 ns_map.pop()
  1027.             elif event == "start":
  1028.                 if root is None:
  1029.                     root = elem
  1030.                 elem.set(NS_MAP, dict(ns_map))
  1031.         return ET.ElementTree(root)
  1032.  
  1033.  
  1034.     def get_resource_type(self, resource_type_url):
  1035.         """Retrieve a resource type by the URL of its description."""
  1036.         xml_id = self.lookup_xml_id(resource_type_url)
  1037.         resource_type = self.resource_types.get(xml_id)
  1038.         if resource_type is None:
  1039.             raise KeyError('No such XML ID: "%s"' % resource_type_url)
  1040.         return resource_type
  1041.  
  1042.     def lookup_xml_id(self, url):
  1043.         """A helper method for locating a part of a WADL document.
  1044.  
  1045.         :param url: The URL (with anchor) of the desired part of the
  1046.         WADL document.
  1047.         :return: The XML ID corresponding to the anchor.
  1048.         """
  1049.         markup_uri = URI(self.markup_url).ensureNoSlash()
  1050.         markup_uri.fragment = None
  1051.  
  1052.         if url.startswith('http'):
  1053.             # It's an absolute URI.
  1054.             this_uri = URI(url).ensureNoSlash()
  1055.         else:
  1056.             # It's a relative URI.
  1057.             this_uri = markup_uri.resolve(url)
  1058.         possible_xml_id = this_uri.fragment
  1059.         this_uri.fragment = None
  1060.  
  1061.         if this_uri == markup_uri:
  1062.             # The URL pointed elsewhere within the same WADL document.
  1063.             # Return its fragment.
  1064.             return possible_xml_id
  1065.  
  1066.         # XXX leonardr 2008-05-28:
  1067.         # This needs to be implemented eventually for Launchpad so
  1068.         # that a script using this client can navigate from a WADL
  1069.         # representation of a non-root resource to its definition at
  1070.         # the server root.
  1071.         raise NotImplementedError("Can't look up definition in another "
  1072.                                   "url (%s)" % url)
  1073.  
  1074.     def get_resource_by_path(self, path):
  1075.         """Locate one of the resources described by this document.
  1076.  
  1077.         :param path: The path to the resource.
  1078.         """
  1079.         # XXX leonardr 2008-05-27 This method only finds top-level
  1080.         # resources. That's all we need for Launchpad because we don't
  1081.         # define nested resources yet.
  1082.         matching = [resource for resource in self.resources
  1083.                     if resource.attrib['path'] == path]
  1084.         if len(matching) < 1:
  1085.             return None
  1086.         if len(matching) > 1:
  1087.             raise WADLError("More than one resource defined with path %s"
  1088.                             % path)
  1089.         return Resource(
  1090.             self, merge(self.resource_base, path, True), matching[0])
  1091.